package ro.gillardon.business.priceservice.impl.train;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;

import org.apache.log4j.Logger;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

import ro.gillardon.business.exception.BusinessException;
import ro.gillardon.commons.Offers;
import ro.gillardon.commons.Price;
import ro.gillardon.commons.config.ConfigParam;
import ro.gillardon.commons.config.ConfigProvider;
import ro.gillardon.commons.logger.BTPLogger;
import ro.gillardon.commons.mot.train.TrainConditions;
import ro.gillardon.commons.mot.train.TrainOfferDetails;

/**
 * Class for parsing the raw HTML result table, returned by navigator into useful TrainOffer objects.
 *
 * @author davidl
 */
public final class DBahnResultParser {

  /** The Constant COLUMN_CLASS_PRICE. */
  private static final String COLUMN_CLASS_PRICE;

  /** The Constant COLUMN_CLASS_TRAIN_TYPE. */
  private static final String COLUMN_CLASS_TRAIN_TYPE;

  /** The Constant COLUMN_CLASS_CHANGES. */
  private static final String COLUMN_CLASS_CHANGES;

  /** The Constant COLUMN_CLASS_DURATION. */
  private static final String COLUMN_CLASS_DURATION;

  /** The Constant COLUMN_CLASS_DATE. */
  private static final String COLUMN_CLASS_DATE;

  /** The Constant COLUMN_CLASS_TIME. */
  private static final String COLUMN_CLASS_TIME;

  /** The Constant ROW_CLASS_FIRST. */
  private static final String ROW_CLASS_FIRST;

  /** The Constant ROW_CLASS_LAST. */
  private static final String ROW_CLASS_LAST;

  static {
    COLUMN_CLASS_PRICE = ConfigProvider.getProperty(ConfigParam.BUSINESS_PRICESERVICE_TRAIN_PARSER_PRICE);
    COLUMN_CLASS_TRAIN_TYPE = ConfigProvider.getProperty(ConfigParam.BUSINESS_PRICESERVICE_TRAIN_PARSER_TRAIN_TYPE);
    COLUMN_CLASS_CHANGES = ConfigProvider.getProperty(ConfigParam.BUSINESS_PRICESERVICE_TRAIN_PARSER_CHANGES);
    COLUMN_CLASS_DURATION = ConfigProvider.getProperty(ConfigParam.BUSINESS_PRICESERVICE_TRAIN_PARSER_DURATION);
    COLUMN_CLASS_DATE = ConfigProvider.getProperty(ConfigParam.BUSINESS_PRICESERVICE_TRAIN_PARSER_DATE);
    COLUMN_CLASS_TIME = ConfigProvider.getProperty(ConfigParam.BUSINESS_PRICESERVICE_TRAIN_PARSER_TIME);
    ROW_CLASS_FIRST = ConfigProvider.getProperty(ConfigParam.BUSINESS_PRICESERVICE_TRAIN_PARSER_FIRST_ROW);
    ROW_CLASS_LAST = ConfigProvider.getProperty(ConfigParam.BUSINESS_PRICESERVICE_TRAIN_PARSER_LAST_ROW);
  }

  /** The Constant ERROR_FORMAT. */
  private static final String ERROR_FORMAT = "No %s attribute found in: %s";

  /**
   * The Logger.
   */
  private static final Logger LOG = Logger.getLogger(DBahnResultParser.class);

  /** Prohibit instantiation. */
  private DBahnResultParser() {
    // prohibit instantiation.
  }

  /**
   * Parses the raw html Source.
   *
   * @param source the raw source
   * @param conditions the train conditions
   * @return the offers
   * @throws BusinessException if page is corrupted
   */
  public static Offers<TrainOfferDetails> parse(final String source, final TrainConditions conditions)
    throws BusinessException {

    String formattedSource = removeSpacesFromBeginningOfClassIdentifiers(source);
    Document doc = Jsoup.parse(formattedSource);

    Elements firsRows = doc.select(ROW_CLASS_FIRST);
    Elements secondRows = doc.select(ROW_CLASS_LAST);

    checkForCorruptedData(firsRows, secondRows);
    return parseEachRow(conditions, firsRows, secondRows);
  }

  /**
   * Parses the each row.
   *
   * @param conditions the conditions
   * @param firsRows the firs rows
   * @param secondRows the second rows
   * @return the offers
   */
  private static Offers<TrainOfferDetails> parseEachRow(final TrainConditions conditions, final Elements firsRows,
    final Elements secondRows) {
    List<TrainOfferDetails> offers = new ArrayList<TrainOfferDetails>();
    for (int i = 0; i < firsRows.size(); i++) {
      offers.add(parseSingleRow(conditions, firsRows, secondRows, i));
    }
    return new Offers<TrainOfferDetails>(offers);
  }

  /**
   * Parses the single row.
   *
   * @param conditions the conditions
   * @param firsRows the firs rows
   * @param secondRows the second rows
   * @param i the i
   * @return the train offer details
   */
  private static TrainOfferDetails parseSingleRow(final TrainConditions conditions, final Elements firsRows,
    final Elements secondRows, final int i) {
    TrainOfferDetails currentOfferDetails;
    currentOfferDetails = constructCompleteDetailInfo(firsRows.get(i), secondRows.get(i));
    currentOfferDetails.setFrom(conditions.getFrom());
    currentOfferDetails.setTo(conditions.getTo());
    return currentOfferDetails;
  }

  /**
   * Check for corrupted data.
   *
   * @param firstRows the first rows
   * @param secondRows the second rows
   * @throws BusinessException the business exception
   */
  private static void checkForCorruptedData(final Elements firstRows, final Elements secondRows)
    throws BusinessException {
    if (firstRows.size() != secondRows.size()) {
      throw new BusinessException("Data retrieval not completed, maybe page is corrupted!");
    }
  }

  /**
   * Removes the spaces from beginning of class identifiers.
   *
   * @param source the source
   * @return the string
   */
  private static String removeSpacesFromBeginningOfClassIdentifiers(final String source) {
    return source.replaceAll("class=\" ", "class=\"");
  }

  /**
   * Construct complete detail info.
   *
   * @param fromRow the from row
   * @param toRow the to row
   * @return the train offer details
   */
  private static TrainOfferDetails constructCompleteDetailInfo(final Element fromRow, final Element toRow) {
    TrainOfferDetails offerToBeFilled = new TrainOfferDetails();
    //@formatter:off
    offerToBeFilled.setStart      (parseDateTime(fromRow));
    offerToBeFilled.setEnd        (parseDateTime(toRow));
    // these methods use Lambda from Java8
    offerToBeFilled.setDuration   (parseElement(Duration.class, DBahnResultParserUtil::parseDuration,   fromRow, COLUMN_CLASS_DURATION,   "Duration"));
    offerToBeFilled.setChanges    (parseElement(Integer.class,  DBahnResultParserUtil::parseInt,        fromRow, COLUMN_CLASS_CHANGES,    "Changes"));
    offerToBeFilled.setTrainTypes (parseElement(String.class,   DBahnResultParserUtil::parseTrainTypes, fromRow, COLUMN_CLASS_TRAIN_TYPE, "Train type"));
    offerToBeFilled.setPrice      (parseElement(Price.class,    DBahnResultParserUtil::parsePrice,      fromRow, COLUMN_CLASS_PRICE,      "Price"));
    //@formatter:on
    return offerToBeFilled;
  }

  /**
   * Parses the date time.
   *
   * @param row the row
   * @return the local date time
   */
  private static LocalDateTime parseDateTime(final Element row) {
    Elements startDates = row.select(COLUMN_CLASS_DATE);
    if (startDates.isEmpty()) {
      BTPLogger.error(LOG, String.format(ERROR_FORMAT, "Date", row.toString()));
      return null;
    }
    Elements startTimes = row.select(COLUMN_CLASS_TIME);
    if (startTimes.isEmpty()) {
      BTPLogger.error(LOG, String.format(ERROR_FORMAT, "Time", row.toString()));
      return null;
    }
    return DBahnResultParserUtil.parseDateTime(startDates.get(0).text(), startTimes.get(0).text());
  }

  /**
   * Parses the element. Using the Java8 features of Lambda expressions.
   *
   * @param <R> the generic type
   * @param clazz needed to define the return type
   * @param function the function to be called
   * @param row the row
   * @param classIdentifier the class identifier
   * @param attribute the attribute
   * @return the result (which will be of type clazz defined as the first parameter)
   */
  private static <R> R parseElement(final Class<R> clazz, final Function<String, R> function, final Element row,
    final String classIdentifier, final String attribute) {
    Elements priceElements = row.select(classIdentifier.replaceAll(" ", "."));
    if (priceElements.isEmpty()) {
      BTPLogger.error(LOG, String.format(ERROR_FORMAT, attribute, row.toString()));
      return null;
    }
    return clazz.cast(function.apply(priceElements.get(0).text()));
  }
}
